((window) => {
    'use strict';

    // LinkCom 领航 通信组件 1.2.0
    // 作者: 虎刺
    // 日期: 2023-04-16 18:30:16
    // 说明: 用于跨域通信, 通过 window.postMessage 实现
    // 依赖: Promise, window.postMessage

    // 创建id
    const createId = (function () {
        let num = 0;
        return () => (++num) + ":" + Math.random().toString(36).substring(2);
    })();

    // 深度冻结对象
    const freezeDeep = (function () {
        function freeze(obj) {
            Object.freeze(obj);
            for (const key in obj) {
                if (typeof obj[key] === "object" && obj[key] != null) {
                    freeze(obj[key]);
                }
            }
        }
        return (obj) => {
            freeze(obj);
            return obj;
        };
    })();



    // 在开发环境下, 打印日志
    let log = function (message, ...args) {
        if (window.location.hostname === "localhost") {
            log = function (message, ...args) {
                if (message instanceof Error) {
                    console.trace();
                }
                console.log(`${window.document.title} - ${new Date().toLocaleTimeString()} LinkCom.${message}`, ...args);
            };
            log(message, ...args);
        } else {
            log = null;
        }
    };



    /**
     * 表示一个信号量, 继承自 Promise, 自身包含 resolve 和 reject 函数, 可以直接解决或拒绝 Promise
     * @extends {Promise}
     * @property {Function} resolve 解决 Promise
     * @property {Function} reject  拒绝 Promise
     */
    class Semaphores extends Promise {
        /**
         * 创建一个信号量
         * @param  {Object} data  附加属性 
         */
        constructor(...args) {
            if (args.length === 0 || (args.length === 1 && typeof args[0] === "object")) {
                const hander = Object.assign({}, args[0]);
                super((resolve, reject) => {
                    hander.resolve = resolve;
                    hander.reject = reject;
                });
                Object.assign(this, hander);
                freezeDeep(this);
            } else {
                super(...args);
            }
        }
    }

    /**
     * 表示一个先进先出的异步队列
     * 使用 push(data) 存入数据, 使用异步方法 async shift() 取出数据, 当没有数据时, 会等待, 直到数据被 push 方法存入
     */
    function AsyncQueue() {
        // 必须使用 new 方式创建
        if (!(this instanceof AsyncQueue)) {
            return new AsyncQueue();
        }
        // 由于 class 的私有属性兼容性不好, 所以使用闭包实现
        const list = [new Semaphores()];

        /**
         * 取出数据, 如果没有数据, 则等待
         * @returns {Object} 存入的数据
         */
        this.shift = async () => {
            if (list.length === 0) {
                throw new Error("queue is destroyed");
            }
            const first = list[0];
            const data = await first;
            if (first === list[0]) {
                list.shift();
            }
            return data;
        }

        /**
         * 存入数据
         * @param {any} data 存入的数据 
         */
        this.push = data => {
            if (list.length === 0) {
                throw new Error("queue is destroyed");
            }
            const last = list[list.length - 1];
            last.resolve(data);
            list.push(new Semaphores());
        }

        /**
         * @returns {Number} 获取队列长度
         */
        this.count = () => list.length;

        /**
         * 销毁队列, 并将所有的 Semaphores 解决为错误
         * @param {String} reason 销毁原因
         */
        this.destroy = reason => {
            const err = reason instanceof Error ? reason : new Error(reason || "queue is destroyed");
            list.forEach(x => x.reject(err));
            list.length = 0;
        }

    }


    /**
     * 消息基类: 表示一个消息
     * @property {String} token       生成消息的处理程序token
     * @property {String} type        消息类型 ("request"/"response")
     * @property {String} id          消息id
     */
    class Message {
        /**
         * 创建一个消息
         * @param {String} token  生成消息的处理程序token
         * @param {String} type   消息类型
         * @param {String} [id]   消息id (可选, 未提供则自动生成)
         * @param {Object} [props] 附加属性
         */
        constructor(token, type, id, props) {
            if (typeof token !== "string") {
                throw new Error("token is not string");
            }
            if (typeof type !== "string" || !["request", "response"].includes(type)) {
                throw new Error("type is not string or not in [request, response]");
            }
            if (typeof id !== "string" || id === "") {
                throw new Error("id is invalid");
            }
            // 将所有值添加当this, 并固化
            Object.assign(this, props, { token, type, id });
            freezeDeep(this);
        }

        /**
         * 构建可用于发送的消息体
         * @returns {Object} 消息体
         */
        buildMessage() {
            return Object.assign({}, this);
        }

        /**
         * 解析消息
         * @param {Object} message 消息体 
         * @returns {Message} 解析后的消息
         */
        static parse(message) {
            if (typeof message !== "object" || message == null) {
                throw new Error("message is not object");
            }
            const { token, type, id } = message;
            return new Message(token, type, id, message);
        }
    }

    /**
     * 表示一个请求消息
     * @extends {Message}
     * @property {String} command 消息命令
     * @property {any}    data    消息数据
     */
    class RequestMessage extends Message {
        /**
         * 创建一个请求消息
         * @param {String} token   生成消息的处理程序token 
         * @param {String} command 消息命令
         * @param {any}    data    消息数据
         */
        constructor(token, command, data) {
            super(token, "request", createId(), { command, data });
        }
    }

    /**
     * 表示一个响应消息
     * @extends {Message}
     * @property {String} id      请求消息的id
     * @property {any}    result  返回值或错误
     */
    class ResponseMessage extends Message {
        constructor(token, id, result) {
            super(token, "response", id, { result });
        }
    }



    /**
     * 表示一个消息处理程序, 用于发送消息和接收消息
     * @property {String} token       消息处理程序token
     * @property {String} targetToken 目标token
     * @property {Object} methods     注册为允许远程调用方法
     * @property {String} url         目标url
     * @property {Window} window      目标window
     */
    class MessageHandler {
        /**
         * 创建一个消息处理程序
         * @param {Object} methods  注册为允许远程调用方法
         * @param {String} [url]    目标url
         * @param {Window} [win]    目标window
         */
        constructor(methods, url, win) {
            if (typeof methods !== "object" || methods == null) {
                throw new Error("methods is not object");
            }
            Object.defineProperties(this, {
                token: { value: createId() },
                methods: { value: methods },
                targetUrl: {
                    get: () => url && url.origin,
                    set(value) {
                        if (url instanceof URL) {
                            url = value;
                        } else if (typeof value === "string") {
                            url = new URL(value);
                        } else {
                            throw new Error("url is not URL");
                        }
                    }
                },
                targetWindow: {
                    get: () => win,
                    set(value) {
                        if (value && value.window === value) {
                            win = value;
                        } else {
                            throw new Error("window is not Window");
                        }
                    }
                },
            });
            if (win != null) {
                this.targetWindow = win;
            }
            if (url != null) {
                this.targetUrl = url;
            }
            window.addEventListener("message", event => {
                // 过滤无效消息
                if (event.data == null || this.onmessage == null) {
                    return;
                }
                log && log(`onmessage: (${JSON.stringify(event.data)}) <- ${event.origin}`)
                // 解析消息 {Message} 类型
                const message = Message.parse(event.data);

                if (this.targetToken != null && (message.token !== this.targetToken || event.origin !== this.targetUrl)) {
                    return;
                }
                this.onmessage(message, event);
            }, false);
        }

        targetUrl = null;

        /**
         * 发送消息
         * @param {Message} message 待发送的消息
         */
        send(message) {
            if (!(message instanceof Message)) {
                throw new Error("message is not Message");
            }
            const data = message.buildMessage();
            // 发送请求
            log && log(`send: ${data.type}, (${JSON.stringify(data)}) -> ${this.targetUrl}`);
            this.targetWindow.postMessage(data, this.targetUrl);
        }

        /**
         * 接收消息回调, 参数为 (message, event)
         */
        onmessage = null;

        /**
         * 执行已注册的方法
         * @param {String} method 方法名  
         * @param {Proxy}  that   方法执行的this
         * @param {Array}  args   方法参数
         * @returns {any} 方法返回值, 如果方法不存在则返回 undefined
         */
        apply(that, method, args) {
            if (typeof method !== "string") {
                throw new Error("method is not string");
            }
            const fn = this.methods[method];
            if (typeof fn !== "function") {
                return undefined;
            }
            const result = fn.apply(that, args);
            return result === undefined ? null : result;
        }
    }

    /**
     * 表示一个任务板, 任务为 Semaphores 类型
     * 每个存入的任务都有一个超时时间, 超时后自动删除, 并将 Semaphores 解决为错误
     * 每个存入的任务可在超时前被完成(完成可以是解决或拒绝), 完成后自动删除
     */
    function TaskBoard() {
        // 必须使用 new 方式创建
        if (!(this instanceof TaskBoard)) {
            return new TaskBoard();
        }
        // 由于 class 的私有属性兼容性不好, 所以使用闭包实现
        const tasks = {};
        /**
         * 加入任务
         * @param {String}      id      任务id
         * @param {Semaphores}  task    消息数据
         * @param {number}      timeout 超时时间
         */
        this.put = (id, task, timeout) => {
            if (this.has(id)) {
                throw new Error("id already exists");
            }
            const timeoutId = setTimeout(() => {
                log && log(`timeout: ${JSON.stringify(task)}`);
                this.finish(id, new Error("timeout"));
            }, timeout);
            tasks[id] = { content: task, timeoutId };
        };
        /**
         * 取出任务并删除任务
         * @param {String} id 任务id
         * @returns {Semaphores} 任务数据
         */
        this.take = id => {
            if (!this.has(id)) {
                return null;
            }
            const data = tasks[id];
            clearTimeout(data.timeoutId);
            delete tasks[id];
            return data.content;
        };
        /**
         * 判断指定id的任务是否存在
         * @param {String} id 任务id
         * @returns {Boolean} true: 存在, false: 不存在
         */
        this.has = id => id in tasks;
        /**
         * @returns {Number} 获取任务数量
         */
        this.count = () => Object.keys(tasks).length;
        /**
         * 清空任务
         */
        this.clear = () => Object.keys(tasks).forEach(id => this.take(id));
        /**
         * 查看任务
         * @param {String} id 任务id
         * @returns {Semaphores} 任务数据, 不存在时返回 空对象 方便解构
         */
        this.get = id => this.has(id) ? tasks[id].content : null;
        /**
         * 完成任务
         * @param {String}   id  任务id
         * @param {any|Error} result  任务结果, 可以是 Error
         * @returns {Boolean} true: 回复成功, false: 任务不存在
         */
        this.finish = (id, result) => {
            const task = this.take(id); // 获取回调
            if (task == null) {
                return false;
            }
            // result 为 Error 时, 触发 reject, 否则触发 resolve
            const exec = result instanceof Error ? task.reject : task.resolve;
            exec(result);
            return true;
        };
    }


    /**
     * 表示一个收发客户端
     * @param {MessageHandler} handler 消息处理程序 , 包含 url, window, token, targetToken
     * @param {Number} [timeout=1000]  超时时间 默认1000ms 最小100ms
     */
    function Client(handler, timeout) {
        // 必须使用 new 方式创建
        if (!(this instanceof Client)) {
            return new Client(handler, timeout);
        }
        // 超时时间 默认1000ms 最小100ms
        timeout = Math.max(100, parseInt(timeout) || 1000);

        // 使用先进先出队列, 实例化收件箱
        const inbox = new AsyncQueue();

        // 实例化任务板
        const tasks = new TaskBoard();

        // 页面关闭时触发: 发送关闭命令
        const beforeunload = () => handler.targetWindow && this.request("close", null, true);
        // 监听消息, 页面关闭时发送关闭命令
        window.addEventListener("beforeunload", beforeunload, false);

        // 收到消息时触发: 处理消息
        handler.onmessage = (message, event) => {
            switch (message.type) {
                case "request":
                    // 执行回调
                    if (inbox.count() > 1000) {
                        this.close("堆积消息过多，保护性关闭");
                        return;
                    }
                    inbox.push({ message, event });
                    break;
                case "response":
                    // 判断消息有效性
                    if (!tasks.has(message.id)) {
                        return;
                    }
                    const { targetToken } = tasks.get(message.id);
                    if (targetToken == null || targetToken === message.token) {
                        tasks.finish(message.id, message.result);
                    }
                    break;
                default:
                    return;
            }
        };

        /**
         * 关闭客户端 
         * @param {String} [reason = "主动断开"] 关闭原因 
         */
        this.close = function (reason) {
            window.removeEventListener("beforeunload", beforeunload);
            window.removeEventListener("message", onmessage, false);
            // 清理资源
            handler.onmessage = null;
            tasks.clear();
            inbox.destroy(reason || "主动断开");
        }


        /**
         * 发送请求消息 第三个参数表示是否忽略响应
         * @param {String} command 消息命令
         * @param {any} data 消息数据
         * @param {Boolean} [unresponse=false] 是否不需要响应
         * @returns 返回一个等待响应的 Promise
         */
        this.request = async function (command, data, unresponse) {
            if (handler.targetWindow == null) {
                throw new Error("handler is not ready");
            }
            const message = new RequestMessage(handler.token, command, data);
            const content = new Semaphores({ message, targetToken: handler.targetToken });
            if (unresponse !== true) {
                tasks.put(message.id, content, timeout);
            }
            handler.send(message);
            return await content;
        }

        /**
         * 发送响应消息
         * @param {String} id     请求id 
         * @param {any}    result 请求的返回值 
         */
        this.response = (id, result) => {
            const message = new ResponseMessage(handler.token, id, result);
            handler.send(message);
        }

        /**
         * 编译一个用于调用远程方法的代理对象
         * @returns {Object} 返回一个代理对象
         */
        this.builder = () => {
            // 创建 Handler 代理对象
            const invoker = new Proxy({}, {
                get: (target, prop) => {
                    // 如果是 MessageHandler 属性, 则直接返回
                    if (["targetUrl", "targetWindow", "token", "targetToken", "then", "catch", "finally"].includes(prop)) {
                        return handler[prop];
                    }
                    return (...args) => this.request("call", { method: prop, args });
                }
            });

            const methods = handler.methods;

            // 命令集 ($ 开头的为预处理命令, 不验证来源)
            const commands = {
                /**
                 * 询问是否允许连接
                 * @param {any} data 询问命令可带任意参数 
                 * @param {Event} event 事件对象 
                 * @returns {String} 如果允许连接, 则返回处理程序 token, 否则返回 null
                 */
                $ask(data, event) {
                    // 如果已经连接, 则不再连接
                    // 如果没有 connect 方法, 则不允许连接
                    if (handler.targetToken != null || !(methods.connect instanceof Function)) {
                        return;
                    }
                    // 调用 ask 方法, 返回 true 或 undefined 允许连接
                    const result = handler.apply(invoker, "ask", [data, event]);
                    if (result === undefined || result === true) {
                        handler.targetToken = event.data.token;
                        handler.targetWindow = event.source;
                        handler.targetUrl = event.origin;
                        return handler.token;
                    }
                },
                /**
                 * 连接
                 * @param {String} data 应答方token 
                 * @param {Event} event  事件对象
                 * @returns {Boolean} 如果连接成功, 则返回 true, 否则不返回任何值
                 */
                connect(data, event) {
                    const result = handler.apply(invoker, "connect", [data, event]);
                    if (result === undefined) {
                        return;
                    }
                    return true;
                },
                /**
                 * 调用方法
                 * @param {any} data 参数 格式 {method: "方法名", args: [参数列表]}
                 * @param {Event} event  事件对象
                 * @returns {any} 返回方法执行结果
                 */
                call(data, event) {
                    const result = handler.apply(invoker, data.method, [...data.args, event]);
                    if (result === undefined) {
                        throw new Error(`method '${data.method}' is not found`);
                    }
                    return result;
                },
                /**
                 * 关闭连接
                 * @param {null} data 关闭请求不提供任何参数 
                 * @param {Event} event  事件对象
                 */
                close(data, event) {
                    handler.apply(invoker, "close", [data, event]);
                    this.close();
                    throw new Error("is closed");
                }
            }

            /**
             * 执行收到的消息
             * @param {RequestMessage} message 对方发送的消息
             * @param {Event}          event   事件对象 
             * @returns 
             */
            async function execRequestMessage(message, event) {
                const { command, data } = message;
                // 非预处理命令 且 token 为空时, 返回 undefined
                if (!command.startsWith("$") && handler.targetToken == null) {
                    return;
                }

                const fn = commands[command];
                if (!(fn instanceof Function)) {
                    return new Error(`command: '${command}' not found`);
                }

                try {
                    let result = fn(data, event);
                    // 防止死循环, 最多执行 100 次
                    for (let i = 0; i < 100; i++) {
                        if (result instanceof Promise) {
                            result = await result;
                        } else if (result instanceof Function) {
                            result = result();
                        } else {
                            return result;
                        }
                    }
                } catch (error) {
                    log && log(error);
                    return new Error(error.message || "error");
                }
            }

            // 空闲时执行
            const idle = window.requestIdleCallback || window.requestAnimationFrame || window.setTimeout;

            // 循环接收消息
            (async () => {
                while (true) {
                    const { message, event } = await inbox.shift();
                    idle(async () => {
                        const result = await execRequestMessage(message, event);
                        if (result !== undefined) {
                            this.response(message.id, result);
                        }
                    });
                }
            })();

            return invoker;
        }
    }


    /**
     * LinkCom 领航
     * @param {Object} methods 可以被远程调用的方法集合 
     * @returns {LinkCom} 调用器
     * @property {Function} connect 连接远程调用器
     * @property {Function} wait 等待远程调用器连接
     * @property {Function} close 关闭调用器
     */
    function LinkCom(methods) {
        if (this instanceof LinkCom === false) {
            return new LinkCom(methods);
        }
        /**
         * 连接window
         * @param {Window} window  远程窗口
         * @param {String} url     远程地址
         * @param {any}    data    连接参数
         * @returns {Invoker}      远程调用器
         * @typedef {Object} Invoker
         * @property {String} token        消息处理程序token
         * @property {Object} methods      消息处理方法
         * @property {String} targetToken  目标token
         * @property {String} targetUrl    目标url
         * @property {Window} targetWindow 目标window
         */
        this.connect = async function (window, url, data) {
            try {
                // 实例化消息处理程序
                const handler = new MessageHandler(methods, url, window);
                const client = new Client(handler);
                const invoker = client.builder();
                const token = await client.request("$ask", data);
                if (typeof token === "string") {
                    handler.targetToken = token;
                    const result = await client.request("connect", handler.token);
                    if (result) {
                        return invoker;
                    }
                }
            } catch (error) {
                log && log(error);
                throw new Error("拒绝连接:" + error.message);
            }
            throw new Error("拒绝连接");
        }
        /**
         * 等待远程调用器连接
         * @returns {LinkCom} 
         */
        this.wait = function () {
            if (methods == null || !(methods.connect instanceof Function)) {
                throw new Error("methods.connect must be a function");
            }
            const handler = new MessageHandler(methods);
            new Client(handler).builder();
            return this;
        }
    }

    window.LinkCom = LinkCom;
})(window);